package com.hmall.search.service.impl;

import com.alibaba.cloud.commons.lang.StringUtils;
import com.alibaba.fastjson.JSON;
import com.hmall.common.pojo.ItemDoc;
import com.hmall.common.request.PortalPageDTO;
import com.hmall.common.response.PageResultVO;
import com.hmall.search.service.SearchService;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder;
import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.SortOrder;
import org.elasticsearch.search.suggest.Suggest;
import org.elasticsearch.search.suggest.SuggestBuilder;
import org.elasticsearch.search.suggest.SuggestBuilders;
import org.elasticsearch.search.suggest.completion.CompletionSuggestion;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import java.io.IOException;
import java.util.*;

/**
 * @description:
 * @Date: 2023/4/9 20:01
 */
@Service
public class SearchServiceImpl implements SearchService {

    @Resource
    private RestHighLevelClient client;


    /**
     * 搜索自动补全
     *
     * @param key
     */
    @Override
    public List<String> getSuggestions(String key) {
        try {
            //1.获取请求对象
            SearchRequest request = new SearchRequest("hmall");
            //2.准备DSL
            request.source().suggest(new SuggestBuilder().addSuggestion(
                    "suggestions",
                    SuggestBuilders.completionSuggestion("suggestion")
                            .prefix(key)
                            .skipDuplicates(true)
                            .size(10)
            ));
            //3.发送请求获取返回结果
            SearchResponse searchResponse = client.search(request, RequestOptions.DEFAULT);
            List<String> suggestionList = new ArrayList<>();
            //4.解析结果
            Suggest suggest = searchResponse.getSuggest();
            //4.1根据名称获取补全结果
            CompletionSuggestion suggestions = suggest.getSuggestion("suggestions");
            //4.2获取options并遍历
            List<CompletionSuggestion.Entry.Option> options = suggestions.getOptions();
            //4.3获取option中的text，也就是补全词条
            for (CompletionSuggestion.Entry.Option option : options) {
                String text = option.getText().toString();
                suggestionList.add(text);
            }
            //结果返回
            return suggestionList;
        } catch (IOException e) {
            throw new RuntimeException("系统故障");
        }

    }

    /**
     * 聚合索引库，查询品牌、分类等信息
     *
     * @param pageDTO
     */
    @Override
    public Map<String, List<String>> getFilters(PortalPageDTO pageDTO) {
        try {
            //1.获取请求对象
            SearchRequest request = new SearchRequest("hmall");
            //聚合范围限定，搜索页面的品牌、城市等信息不应该是在页面写死，而是通过聚合索引库中的酒店数据得来的：
            buildBasicQuery(pageDTO, request);
            //2.准备DSL
            request.source().size(0);
            //聚合
            buildAggregation(request);
            //3.发送请求，返回结果
            SearchResponse searchResponse = client.search(request, RequestOptions.DEFAULT);
            //4.解析结果
            Map<String, List<String>> result = new HashMap<>(4);
            //解析聚合结果
            Aggregations aggregations = searchResponse.getAggregations();
            //封装结果
            List<String> brandLis = getAggByName(aggregations, "brandAgg");
            List<String> categoryList = getAggByName(aggregations, "categoryAgg");
            result.put("brand", brandLis);
            result.put("category", categoryList);
            //返回结果
            return result;
        } catch (IOException e) {
            throw new RuntimeException("系统异常");
        }
    }

    /**
     * 解析聚合结果
     *
     * @param aggregations
     * @param aggName
     * @return
     */
    private List<String> getAggByName(Aggregations aggregations, String aggName) {
        //根据名称获取聚合结果
        Terms brandTerms = aggregations.get(aggName);
        //获取桶数据
        List<? extends Terms.Bucket> buckets = brandTerms.getBuckets();
        //创建集合存放桶的value
        List<String> value = new ArrayList<>(buckets.size());
        for (Terms.Bucket bucket : buckets) {
            value.add(bucket.getKeyAsString());
        }
        return value;
    }

    /**
     * 聚合
     *
     * @param request
     */
    private void buildAggregation(SearchRequest request) {
        request.source().aggregation(
                AggregationBuilders
                        .terms("brandAgg")
                        .field("brand")
                        .size(100));
        request.source().aggregation(
                AggregationBuilders
                        .terms("categoryAgg")
                        .field("category")
                        .size(100));
    }

    /**
     * 查询商品信息
     *
     * @param pageDTO
     * @return
     */
    @Override
    public PageResultVO getSearchResult(PortalPageDTO pageDTO) {
        try {
            //1.获取请求对象
            SearchRequest request = new SearchRequest("hmall");
            //2.准备DSL
            buildBasicQuery(pageDTO, request);
            //2.1准备分页DSL
            Integer page = (pageDTO.getPage() - 1) * pageDTO.getSize();
            Integer size = pageDTO.getSize();
            request.source().from(page).size(size);
            //2.2按条件排序，默认、销量、价格
            String sortBy = pageDTO.getSortBy();
            if ("sold".equals(sortBy)) {
                request.source().sort(pageDTO.getSortBy(), SortOrder.DESC);
            } else if ("price".equals(sortBy)) {
                request.source().sort(pageDTO.getSortBy(), SortOrder.ASC);
            }
            //4.1高亮
            request.source().highlighter(new HighlightBuilder().field("name").requireFieldMatch(false));
            //3.发送请求，获取结果
            SearchResponse result = client.search(request, RequestOptions.DEFAULT);
            //4.解析结果
            //返回结果
            return handleResult(result);
        } catch (IOException e) {
            throw new RuntimeException();
        }

    }

    /**
     * 准备过滤条件
     *
     * @param pageDTO
     * @param request
     */
    private void buildBasicQuery(PortalPageDTO pageDTO, SearchRequest request) {
        //复合布尔查询。获取boolQuery
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
        //获取请求参数
        String key = pageDTO.getKey();
        String category = pageDTO.getCategory();
        String brand = pageDTO.getBrand();
        Integer minPrice = pageDTO.getMinPrice();
        Integer maxPrice = pageDTO.getMaxPrice();

        //判断是否有传条件，如果key不为空，那么要做条件查询
        if (StringUtils.isBlank(key)) {
            boolQuery.must(QueryBuilders.matchAllQuery());
        } else {
            boolQuery.must(QueryBuilders.matchQuery("all", key));
        }
        //判断分类是否存在
        if (StringUtils.isNotEmpty(category)) {
            boolQuery.filter(QueryBuilders.termQuery("category", category));
        }
        //判断品牌是否存在
        if (StringUtils.isNotEmpty(brand)) {
            boolQuery.filter(QueryBuilders.termQuery("brand", brand));
        }
        //判断最大价格和最小价格
        if (Objects.nonNull(minPrice) && Objects.nonNull((maxPrice))) {
            //前端传过来的价格单位是元，数据库价格单位是分，所以需要乘以100.
            boolQuery.filter(QueryBuilders.rangeQuery("price").gte(minPrice * 100)
                    .lte(maxPrice == 0 ? maxPrice + 88888888 : maxPrice * 100));
        }

        // 2.算分控制
        FunctionScoreQueryBuilder functionScoreQuery =
                QueryBuilders.functionScoreQuery(
                        // 原始查询，相关性算分的查询
                        boolQuery,
                        // function score的数组
                        new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
                                // 其中的一个function score 元素
                                new FunctionScoreQueryBuilder.FilterFunctionBuilder(
                                        // 过滤条件
                                        QueryBuilders.termQuery("isAD", true),
                                        // 算分函数
                                        ScoreFunctionBuilders.weightFactorFunction(10)
                                )
                        });
        request.source().query(functionScoreQuery);
    }

    /**
     * 解析查询结果
     *
     * @param result
     */
    private PageResultVO handleResult(SearchResponse result) {
        SearchHits hits = result.getHits();
        //获取总条数
        long count = hits.getTotalHits().value;
        System.out.println("获取总条数：" + count);
        //获取文档数组
        SearchHit[] hits1 = hits.getHits();
        //创建文档对象集合，收集所有文档
        List<ItemDoc> hotelDocList = new ArrayList<>(hits1.length);
        //遍历封装文档
        for (SearchHit hit : hits1) {
            //对获取的文档反序列化
            ItemDoc itemDoc = JSON.parseObject(hit.getSourceAsString(), ItemDoc.class);
            //获取高亮结果
            Map<String, HighlightField> highlightFields = hit.getHighlightFields();
            //判断是否有高亮
            if (!CollectionUtils.isEmpty(highlightFields)) {
                //根据字段名获取高亮结果
                HighlightField highlightField = highlightFields.get("name");
                //获取高亮值
                if (null != highlightField && highlightField.getFragments().length > 0) {
                    String name = highlightField.getFragments()[0].toString();
                    //覆盖高亮结果
                    itemDoc.setName(name);
                }

            }
            //收集文档
            hotelDocList.add(itemDoc);
        }
        //封装并返回结果
        return new PageResultVO(count, hotelDocList);
    }
}